diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
index a4fb6569ac65b849cf4d876bec4c997481970ebd..2490d3155134deb9dc0bcb72ce4b13d2538e9b79 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -5,7 +5,11 @@ import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.ListNodeData;
 import it.inaf.ia2.vospace.ui.service.NodesService;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.Property;
@@ -19,7 +23,6 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -51,13 +54,6 @@ public class NodesController extends BaseController {
         return ResponseEntity.ok(nodesService.generateNodesHtml(path, principal));
     }
 
-    @DeleteMapping(value = {"/nodes", "/nodes/**"})
-    public void deleteNode() {
-        String path = getPath("/nodes/");
-        LOG.debug("deleteNode called for path {}", path);
-        client.deleteNode(path);
-    }
-
     @GetMapping(value = "/download/**")
     public ResponseEntity<?> directDownload() {
 
@@ -100,6 +96,32 @@ public class NodesController extends BaseController {
         client.createNode(node);
     }
 
+    @PostMapping(value = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<?> deleteNodes(@RequestBody List<String> nodesToDelete) {
+
+        CompletableFuture[] deleteCalls = nodesToDelete.stream()
+                .map(nodeToDelete -> {
+                    return CompletableFuture.runAsync(() -> {
+                        LOG.debug("deleteNode called for path {}", nodeToDelete);
+                        client.deleteNode(nodeToDelete);
+                    }, Runnable::run);
+                })
+                .collect(Collectors.toList())
+                .toArray(CompletableFuture[]::new);
+
+        Optional<RuntimeException> error = CompletableFuture.allOf(deleteCalls)
+                .handle((fn, ex) -> {
+                    return Optional.ofNullable(ex == null ? null : new RuntimeException(ex));
+                }).join();
+
+        if (error.isPresent()) {
+            throw error.get();
+        }
+
+        // All the nodes have been correctly deleted
+        return ResponseEntity.noContent().build();
+    }
+    
     protected String getPath(String prefix) {
         String requestURL = servletRequest.getRequestURL().toString();
         return NodeUtils.getPathFromRequestURLString(requestURL, prefix);
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
index f341a75883e3a15f6821e1a5313e00bb54be7d3e..0934c8d16138e14cb386a8b40d16db4764277cca 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
@@ -65,10 +65,14 @@ public class NodesService {
             return "";
         }
 
+        boolean deletable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !nodeInfo.isSticky();
+
         String html = "<tr>";
         html += "<td><input type=\"checkbox\" data-node=\"" + nodeInfo.getPath().replace("\"", "\\\"") + "\" ";
         if (nodeInfo.isAsyncTrans()) {
             html += "class=\"async\"";
+        } else if (deletable) {
+            html += "class=\"deletable\"";
         }
         html += "/></td>";
         html += "<td>" + getIcon(nodeInfo) + getLink(nodeInfo, user) + "</td>";
@@ -76,7 +80,7 @@ public class NodesService {
         html += "<td>" + nodeInfo.getGroupRead() + "</td>";
         html += "<td>" + nodeInfo.getGroupWrite() + "</td>";
         html += "<td>";
-        if (NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !nodeInfo.isSticky()) {
+        if (deletable) {
             html += "<span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span>";
         }
         html += "</td>";
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
index 79b0fada9c875c4b8b21bd05a3021fc594191c80..39e3863a578840147ef792f196675ee67457c515 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
@@ -1,25 +1,41 @@
 package it.inaf.ia2.vospace.ui.controller;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.service.NodesService;
+import java.util.Arrays;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.Test;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MockMvc;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import org.springframework.web.util.NestedServletException;
 
 @SpringBootTest
 @AutoConfigureMockMvc
 public class NodesControllerTest {
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     @MockBean
     private NodesService nodesService;
 
+    @MockBean
+    private VOSpaceClient client;
+
     @Autowired
     private MockMvc mockMvc;
 
@@ -49,4 +65,37 @@ public class NodesControllerTest {
 
         verify(nodesService).generateNodesHtml(eq("/a/b/c"), any());
     }
+
+    @Test
+    public void testDeleteMultipleNodes() throws Exception {
+
+        List<String> paths = Arrays.asList("/a/b/c", "/e/f/g");
+
+        mockMvc.perform(post("/delete")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(MAPPER.writeValueAsString(paths)))
+                .andExpect(status().isNoContent());
+
+        verify(client, times(1)).deleteNode(eq("/a/b/c"));
+        verify(client, times(1)).deleteNode(eq("/e/f/g"));
+    }
+
+    @Test
+    public void testErrorOnDelete() throws Exception {
+
+        doThrow(new RuntimeException())
+                .when(client).deleteNode(any());
+
+        boolean exception = false;
+        try {
+            mockMvc.perform(post("/delete")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(MAPPER.writeValueAsString(Arrays.asList("/test"))))
+                    .andReturn();
+        } catch (NestedServletException ex) {
+            exception = true;
+        }
+
+        assertTrue(exception);
+    }
 }
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/root.json b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
index dcd4c04815c6d060abf98aa7579b38931f4392c2..6014d41b8781cdf24bfc7e2d5c4ed57c399f978a 100644
--- a/vospace-ui-frontend/src/api/mock/data/nodes/root.json
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
@@ -1,4 +1,4 @@
 {
   "writable": true,
-  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td>      <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
+  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"deletable\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td>      <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
 }
diff --git a/vospace-ui-frontend/src/api/mock/index.js b/vospace-ui-frontend/src/api/mock/index.js
index e92cd1114526de3592eb3d63d6ef420d6526bac6..da818a1244c370383e099314882f89336d91deca 100644
--- a/vospace-ui-frontend/src/api/mock/index.js
+++ b/vospace-ui-frontend/src/api/mock/index.js
@@ -55,10 +55,10 @@ export default {
   uploadFile() {
     return fetch({});
   },
-  deleteNode() {
+  deleteNodes() {
     return fetch({});
   },
   keepalive() {
-    return fetch({});    
+    return fetch({});
   }
 }
diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js
index 0cad25df45dfecb676b3c7638f9c41a9a545b6a4..23878170ddaacd4370fc4d8efa7899a2addbeee7 100644
--- a/vospace-ui-frontend/src/api/server/index.js
+++ b/vospace-ui-frontend/src/api/server/index.js
@@ -130,15 +130,16 @@ export default {
       }
     })
   },
-  deleteNode(path) {
-    let url = BASE_API_URL + 'nodes' + escapePath(path);
+  deleteNodes(paths) {
+    let url = BASE_API_URL + 'delete';
     return apiRequest({
-      method: 'DELETE',
+      method: 'POST',
       url: url,
       withCredentials: true,
       headers: {
         'Cache-Control': 'no-cache'
-      }
+      },
+      data: paths
     }, true, true);
   },
   keepalive() {
diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue
index 3038c24d201c005f6a020af920a68018a5e32276..cc35967a89dc5fac3cb99d888fc06de7713fdf6c 100644
--- a/vospace-ui-frontend/src/components/Main.vue
+++ b/vospace-ui-frontend/src/components/Main.vue
@@ -5,6 +5,7 @@
     <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.create-folder-modal>New folder</b-button>
     <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.upload-files-modal>Upload files</b-button>
     <b-button variant="primary" class="mr-2" v-if="asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-button>
+    <b-button variant="danger" class="mr-2" v-if="deleteButtonEnabled" @click="deleteNodes">Delete</b-button>
   </div>
   <b-card>
     <table class="table b-table table-striped table-hover">
@@ -69,6 +70,9 @@ export default {
     asyncButtonEnabled() {
       return this.$store.state.asyncButtonEnabled;
     },
+    deleteButtonEnabled() {
+      return this.$store.state.deleteButtonEnabled;
+    },
     writable() {
       return this.$store.state.writable;
     }
@@ -97,6 +101,15 @@ export default {
     },
     startAsyncRecallJob() {
       this.$store.dispatch('startAsyncRecallJob');
+    },
+    deleteNodes() {
+      let deletableCheckboxes = document.querySelectorAll('#nodes input.deletable:checked');
+      let paths = [];
+      for (let i = 0; i < deletableCheckboxes.length; i++) {
+        paths.push(deletableCheckboxes[i].getAttribute('data-node'));
+      }
+      this.$store.commit('setNodesToDelete', paths);
+      this.$bvModal.show('confirm-delete-modal');
     }
   }
 }
diff --git a/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue b/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue
index fb403398a9330225f21ea80cf5178834622cf977..69c448edca0e003b87bdcdcb4ab0e2dfb02a756e 100644
--- a/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue
+++ b/vospace-ui-frontend/src/components/modal/ConfirmDeleteModal.vue
@@ -1,6 +1,11 @@
 <template>
-<b-modal id="confirm-delete-modal" title="Confirm delete" okTitle="Yes, delete" @ok="deleteNode" @hidden="reset" ok-variant="danger">
-  <p>Do you really want to delete node at {{nodeToDelete}}?</p>
+<b-modal id="confirm-delete-modal" title="Confirm delete" okTitle="Yes, delete" @ok="deleteNodes" @hidden="reset" ok-variant="danger">
+  <p>Do you really want to delete the following nodes?</p>
+  <p>
+  <ul>
+    <li v-for="node in nodesToDelete" :key="node">{{node}}</li>
+  </ul>
+  </p>
 </b-modal>
 </template>
 
@@ -8,17 +13,17 @@
 export default {
   name: 'ConfirmDeleteModal',
   computed: {
-    nodeToDelete() { return this.$store.state.nodeToDelete }
+    nodesToDelete() { return this.$store.state.nodesToDelete }
   },
   methods: {
     reset() {
-      this.$store.commit('setNodeToDelete', null);
+      this.$store.commit('setNodesToDelete', []);
     },
-    deleteNode(event) {
+    deleteNodes(event) {
       // Prevent modal from closing
       event.preventDefault();
 
-      this.$store.dispatch('deleteNode')
+      this.$store.dispatch('deleteNodes')
         .then(() => {
           this.$bvModal.hide('confirm-delete-modal');
         });
diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js
index d93a3b3c1a0f45774607da83c6f76a42b88cb87f..4c7c315753a530134388dbe887e95e4f9fc4f3ef 100644
--- a/vospace-ui-frontend/src/main.js
+++ b/vospace-ui-frontend/src/main.js
@@ -19,7 +19,7 @@ let vm = new Vue({
 }).$mount('#app')
 
 window.deleteNode = function(path) {
-  store.commit('setNodeToDelete', path);
+  store.commit('setNodesToDelete', [path]);
   vm.$bvModal.show('confirm-delete-modal');
 }
 
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index f1278db7c7f43fc1f22bbb697d088c0807992558..87f5478eaf1a3a7ffb8e4bd847fd3338dd99aff0 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -7,15 +7,25 @@ import main from './main';
 
 Vue.use(Vuex);
 
+function updateArray(oldArr, newArr) {
+  // empty the array
+  oldArr.splice(0, oldArr.length);
+  // fill again
+  for (let i = 0; i < newArr.length; i++) {
+    oldArr.push(newArr[i]);
+  }
+}
+
 export default new Vuex.Store({
   state: {
     path: '',
     loading: true,
     asyncButtonEnabled: false,
+    deleteButtonEnabled: false,
     jobs: [],
     jobsLoading: true,
     user: 'anonymous',
-    nodeToDelete: null,
+    nodesToDelete: [],
     writable: false
   },
   mutations: {
@@ -31,13 +41,11 @@ export default new Vuex.Store({
     setAsyncButtonEnabled(state, value) {
       state.asyncButtonEnabled = value;
     },
+    setDeleteButtonEnabled(state, value) {
+      state.deleteButtonEnabled = value;
+    },
     setJobs(state, jobs) {
-      // empty the array
-      state.jobs.splice(0, jobs.length);
-      // fill again
-      for (let i = 0; i < jobs.length; i++) {
-        state.jobs.push(jobs[i]);
-      }
+      updateArray(state.jobs, jobs);
     },
     addJob(state, job) {
       state.jobs.push(job);
@@ -48,8 +56,8 @@ export default new Vuex.Store({
     setUsername(state, username) {
       state.user = username;
     },
-    setNodeToDelete(state, path) {
-      state.nodeToDelete = path;
+    setNodesToDelete(state, paths) {
+      updateArray(state.nodesToDelete, paths);
     },
     setWritable(state, value) {
       state.writable = value;
@@ -65,14 +73,15 @@ export default new Vuex.Store({
           let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]');
           for (let i = 0; i < checkboxes.length; i++) {
             checkboxes[i].addEventListener('change', function() {
-              dispatch('computeButtonVisibility');
+              dispatch('computeButtonsVisibility');
             });
           }
-          dispatch('computeButtonVisibility');
+          dispatch('computeButtonsVisibility');
         });
     },
-    computeButtonVisibility({ commit }) {
+    computeButtonsVisibility({ commit }) {
       commit('setAsyncButtonEnabled', document.querySelectorAll('#nodes input.async:checked').length > 0);
+      commit('setDeleteButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0);
     },
     startAsyncRecallJob({ commit }) {
       let asyncCheckboxes = document.querySelectorAll('#nodes input.async:checked');
@@ -119,8 +128,8 @@ export default new Vuex.Store({
           dispatch('setPath', state.path);
         });
     },
-    deleteNode({ state, dispatch }) {
-      client.deleteNode(state.nodeToDelete)
+    deleteNodes({ state, dispatch }) {
+      client.deleteNodes(state.nodesToDelete)
         .then(() => {
           // Reload current node
           dispatch('setPath', state.path);