diff --git a/app-openbis-command-line/src/python/obis/dm/commands/upload.py b/app-openbis-command-line/src/python/obis/dm/commands/upload.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9ee708b584e2d329a364ed97b320948ac29f63c
--- /dev/null
+++ b/app-openbis-command-line/src/python/obis/dm/commands/upload.py
@@ -0,0 +1,46 @@
+#   Copyright ETH 2023 Zürich, Scientific IT Services
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+
+from .openbis_command import OpenbisCommand
+from ..command_result import CommandResult
+from ..utils import cd
+from ...scripts.click_util import click_echo
+
+
+class Upload(OpenbisCommand):
+    """
+    Command to upload physical files to form a data set.
+    """
+
+    def __init__(self, dm, sample_id, data_set_type, files):
+        """
+        :param dm: data management
+        :param sample_id: permId or sample path of the parent sample
+        :param data_set_type: type of newly created data set.
+        :param files: list of files/directories to upload
+        """
+        self.data_set_type = data_set_type
+        self.files = files
+        self.sample_id = sample_id
+        self.load_global_config(dm)
+        super(Upload, self).__init__(dm)
+
+    def run(self):
+        with cd(self.data_mgmt.invocation_path):
+            click_echo(f"Uploading files {self.files} under {self.sample_id}")
+            ds = self.openbis.new_dataset(type=self.data_set_type, sample=self.sample_id,
+                                          files=self.files)
+            result = ds.save()
+            return CommandResult(returncode=0, output=f"Upload finished. New dataset: {result}")
diff --git a/app-openbis-command-line/src/python/obis/dm/data_mgmt.py b/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
index 93ed3746e1cb39b894cfc25787efd8d253ed868b..326d8bda283081d2c275b36b00711f093e7e64bb 100644
--- a/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
+++ b/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
@@ -35,6 +35,7 @@ from .commands.move import Move
 from .commands.openbis_sync import OpenbisSync
 from .commands.removeref import Removeref
 from .commands.search import Search
+from .commands.upload import Upload
 from .git import GitWrapper
 from .utils import Type
 from .utils import cd
@@ -217,6 +218,15 @@ class AbstractDataMgmt(metaclass=abc.ABCMeta):
         """
         return
 
+    @abc.abstractmethod
+    def upload(self, sample_id, data_set_type, files):
+        """Upload files/directories into a new data set.
+        :param sample_id: permId or sample path of the parent sample
+        :param data_set_type: type of created data set
+        :param files: list of files/directories to upload
+        """
+        return
+
     @abc.abstractmethod
     def search_object(self, type_code, space, project, experiment, property_code, property_value,
                       save):
@@ -231,6 +241,7 @@ class AbstractDataMgmt(metaclass=abc.ABCMeta):
         """
         return
 
+    @abc.abstractmethod
     def search_data_set(self, type_code, space, project, experiment, property_code, property_value,
                         save):
         """Search for datasets in openBIS using filtering criteria.
@@ -290,6 +301,9 @@ class NoGitDataMgmt(AbstractDataMgmt):
     def search_data_set(self, *_):
         self.error_raise("search", "No git command found.")
 
+    def upload(self, *_):
+        self.error_raise("upload", "No git command found.")
+
 
 def restore_signal_handler(data_mgmt):
     data_mgmt.restore()
@@ -575,6 +589,9 @@ class GitDataMgmt(AbstractDataMgmt):
     def search_data_set(self, *_):
         self.error_raise("search", "This functionality is not implemented for data of LINK type.")
 
+    def upload(self, *_):
+        self.error_raise("upload", "This functionality is not implemented for data of LINK type.")
+
 
 class PhysicalDataMgmt(AbstractDataMgmt):
     """DataMgmt operations for DSS-stored data."""
@@ -624,6 +641,10 @@ class PhysicalDataMgmt(AbstractDataMgmt):
         cmd = DownloadPhysical(self, data_set_id, file)
         return cmd.run()
 
+    def upload(self, sample_id, data_set_type, files):
+        cmd = Upload(self, sample_id, data_set_type, files)
+        return cmd.run()
+
     def search_object(self, type_code, space, project, experiment, property_code, property_value,
                       save):
         cmd = Search(self, type_code, space, project, experiment, property_code, property_value,
diff --git a/app-openbis-command-line/src/python/obis/scripts/cli.py b/app-openbis-command-line/src/python/obis/scripts/cli.py
index b081071c8e1c2aa65abb1f92babe0c7151a3ee88..db38e2935f458b0b02e5e847d200362cec58ee80 100644
--- a/app-openbis-command-line/src/python/obis/scripts/cli.py
+++ b/app-openbis-command-line/src/python/obis/scripts/cli.py
@@ -748,7 +748,7 @@ def removeref(ctx, data_set_id, repository):
                repository=repository)
 
 
-# data set commands: download / clone
+# data set commands: download, upload, clone
 
 # download
 
@@ -763,7 +763,7 @@ _download_params = [
 ]
 
 
-@data_set.command("download", short_help="Download files of a linked data set.")
+@data_set.command("download", short_help="Download files of a data set.")
 @add_params(_download_params)
 @click.pass_context
 def data_set_download(ctx, content_copy_index, file, data_set_id, skip_integrity_check):
@@ -772,7 +772,7 @@ def data_set_download(ctx, content_copy_index, file, data_set_id, skip_integrity
                                                         skip_integrity_check))
 
 
-@cli.command(short_help="Download files of a linked data set.")
+@cli.command("download", short_help="Download files of a data set.")
 @add_params(_download_params)
 @click.pass_context
 def download(ctx, content_copy_index, file, data_set_id, skip_integrity_check):
@@ -781,6 +781,33 @@ def download(ctx, content_copy_index, file, data_set_id, skip_integrity_check):
                data_set_id=data_set_id, skip_integrity_check=skip_integrity_check)
 
 
+# upload
+
+
+_upload_params = [
+    click.option(
+        '-f', '--file', "files", help='file or directory to upload.', required=True, multiple=True),
+    click.argument('sample_id'),
+    click.argument('data_set_type'),
+]
+
+
+@data_set.command("upload", short_help="Upload files to form a data set.")
+@add_params(_upload_params)
+@click.pass_context
+def data_set_upload(ctx, sample_id, data_set_type, files):
+    return ctx.obj['runner'].run("upload",
+                                 lambda dm: dm.upload(sample_id, data_set_type, files))
+
+
+@cli.command("upload", short_help="Upload files to form a data set.")
+@add_params(_upload_params)
+@click.pass_context
+def download(ctx, sample_id, data_set_type, files):
+    ctx.obj['runner'] = DataMgmtRunner(ctx.obj, halt_on_error_log=False)
+    ctx.invoke(data_set_upload, files=files, sample_id=sample_id, data_set_type=data_set_type)
+
+
 # clone