diff --git a/src/api/model.ts b/src/api/model.ts
index 3d4fb32821a577bbf9d0255a27d7dfb44e00ea14..3464e8d5c194613fc17a162c6a96c7c37f548458 100644
--- a/src/api/model.ts
+++ b/src/api/model.ts
@@ -4,6 +4,7 @@ import { Model, ModelVersion } from '@/types/model'
 import { PageNumberPaginationParameters, unique } from '.'
 
 export type CreateModelPayload = Pick<Model, 'name'> & Partial<Pick<Model, 'description'>>
+export type UpdateModelPayload = Pick<Model, 'name' | 'description'>
 
 export const createModel = unique(async (params: CreateModelPayload): Promise<Model> => (await axios.post('/models/', params)).data)
 
@@ -19,6 +20,8 @@ export interface ModelListParameters extends PageNumberPaginationParameters {
   name?: string
 }
 
+export const updateModel = unique(async (id: UUID, params: UpdateModelPayload): Promise<Model> => (await axios.patch(`/model/${id}/`, params)).data)
+
 export const listModels = unique(async (params: ModelListParameters): Promise<PageNumberPagination<Model>> => (await axios.get('/models/', { params })).data)
 
 export const retrieveModel = unique(async (id: UUID): Promise<Model> => (await axios.get(`/model/${id}/`)).data)
diff --git a/src/components/Model/EditForm.vue b/src/components/Model/EditForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..57e6dd84b6c700b976365649cc0c95b7339736aa
--- /dev/null
+++ b/src/components/Model/EditForm.vue
@@ -0,0 +1,140 @@
+<template>
+  <slot name="action">
+    <button
+      class="button"
+      v-on:click="openModal = allowEdition"
+      v-bind="$attrs"
+      :disabled="!allowEdition || null"
+      :title="allowTitle"
+    >
+      <i class="icon-edit"></i>
+    </button>
+  </slot>
+
+  <Modal v-model="openModal" :title="modalTitle">
+    <form class="form" v-on:submit.prevent="update">
+      <div class="field">
+        <label class="label">Name</label>
+        <div class="control">
+          <input
+            class="input"
+            :class="{ 'is-danger': fieldErrors.name }"
+            type="text"
+            maxlength="100"
+            v-model.trim="payload.name"
+            :disabled="loading || null"
+          />
+          <template v-if="fieldErrors.name">
+            <p class="help is-danger" v-for="err in fieldErrors.name" :key="err">{{ err }}</p>
+          </template>
+        </div>
+      </div>
+
+      <div class="field">
+        <label class="label">Description</label>
+        <div class="control">
+          <textarea
+            class="textarea"
+            :class="{ 'is-danger': fieldErrors.description }"
+            type="text"
+            v-model.trim="payload.description"
+            :disabled="loading || null"
+          ></textarea>
+          <template v-if="fieldErrors.description">
+            <p class="help is-danger" v-for="err in fieldErrors.description" :key="err">{{ err }}</p>
+          </template>
+        </div>
+      </div>
+    </form>
+
+    <template v-slot:footer="{ close }">
+      <button class="button" v-on:click="close">Cancel</button>
+      <button
+        type="submit"
+        v-on:click="update"
+        class="button is-primary ml-auto"
+        :class="{ 'is-loading': loading }"
+        :disabled="loading || null"
+      >
+        Update
+      </button>
+    </template>
+  </Modal>
+</template>
+
+<script lang="ts">
+import { mapActions } from 'pinia'
+import { errorParser, ensureArray } from '@/helpers'
+import Modal from '@/components/Modal.vue'
+import { useNotificationStore, useModelStore } from '@/stores'
+import { UpdateModelPayload } from '@/api'
+import { PropType, defineComponent } from 'vue'
+import { isAxiosError } from 'axios'
+import { Model } from '@/types/model'
+
+export default defineComponent({
+  components: {
+    Modal
+  },
+  props: {
+    model: {
+      type: Object as PropType<Model>,
+      required: true
+    }
+  },
+  data: () => ({
+    openModal: false,
+    payload: {
+      name: '',
+      description: ''
+    } as UpdateModelPayload,
+    loading: false,
+    fieldErrors: {} as Partial<Record<keyof UpdateModelPayload, string[]>>
+  }),
+  computed: {
+    allowEdition () {
+      return this.model.rights.includes('write')
+    },
+    allowTitle () {
+      return this.allowEdition
+        ? 'Edit model'
+        : 'A contributor access level is required to edit a model'
+    },
+    modalTitle () {
+      return `Edit model "${this.model.name}"`
+    }
+  },
+  methods: {
+    ...mapActions(useModelStore, ['updateModel']),
+    ...mapActions(useNotificationStore, ['notify']),
+    async update () {
+      if (this.loading) return
+      this.loading = true
+      this.fieldErrors = {}
+      try {
+        await this.updateModel(this.model.id, this.payload)
+        this.notify({ type: 'info', text: 'Model updated successfully.' })
+        this.openModal = false
+      } catch (err) {
+        this.notify({ type: 'error', text: errorParser(err) })
+        if (isAxiosError(err)) {
+          this.fieldErrors = err?.response?.data
+            ? Object.fromEntries(Object.entries(err?.response?.data).map(([k, v]) => [k, ensureArray(v)]))
+            : {}
+        }
+      } finally {
+        this.loading = false
+      }
+    }
+  },
+  watch: {
+    model: {
+      immediate: true,
+      handler () {
+        this.payload.name = this.model.name
+        this.payload.description = this.model.description
+      }
+    }
+  }
+})
+</script>
diff --git a/src/stores/model.ts b/src/stores/model.ts
index e38c9ba63d23f9139abbfceacbafbb2272adfb57..f4f22225c5cb83aa7fe72c918f72b1be5a78a6c5 100644
--- a/src/stores/model.ts
+++ b/src/stores/model.ts
@@ -4,8 +4,10 @@ import { UUID } from '@/types'
 import { Model, ModelVersion } from '@/types/model'
 import {
   CreateModelPayload,
+  UpdateModelPayload,
   ModelListParameters,
   createModel,
+  updateModel,
   listModels,
   listModelVersions,
   retrieveModel,
@@ -31,6 +33,12 @@ export const useModelStore = defineStore('model', {
       return model
     },
 
+    async updateModel (id: UUID, params: UpdateModelPayload) {
+      const model = await updateModel(id, params)
+      this.models[model.id] = model
+      return model
+    },
+
     async listModels (params: ModelListParameters) {
       const resp = await listModels(params)
       this.models = {
@@ -69,4 +77,4 @@ export const useModelStore = defineStore('model', {
       }
     }
   }
-})
\ No newline at end of file
+})
diff --git a/src/views/Model/List.vue b/src/views/Model/List.vue
index f1aa09a23f9c6c4d8db902ac3bcea5dbafdd91c7..60af1160408dddf0ea86cb7d73983a308e95e47c 100644
--- a/src/views/Model/List.vue
+++ b/src/views/Model/List.vue
@@ -70,8 +70,9 @@
                   v-on:click="selectedModel = model.id"
                   class="is-clickable"
                   :class="{ 'is-selected': selectedModel === model.id }"
+                  :title="model.name"
                 >
-                  <td :title="model.name">
+                  <td>
                     {{ truncateLong(model.name) }}
                   </td>
                 </tr>
diff --git a/src/views/Model/Model.vue b/src/views/Model/Model.vue
index 0d5964f70bb38d91df0d56d7bdcab297f0bb41c0..df5404b72df279f608e19702de294be1c0dfaf23 100644
--- a/src/views/Model/Model.vue
+++ b/src/views/Model/Model.vue
@@ -5,7 +5,10 @@
         <i class="icon-arrow-left"></i>
         Models
       </router-link>
-      <div class="title">{{ model.name }}</div>
+      <div class="title">
+        {{ model.name }}
+        <EditForm class="ml-2 is-primary" :model="model" />
+      </div>
       <p class="subtitle is-5">Model <ItemId :item-id="model.id" /></p>
 
       <div class="columns">
@@ -51,6 +54,7 @@ import { UUID_REGEX } from '@/config'
 import { ago, errorParser } from '@/helpers'
 import { UUID } from '@/types'
 import Model from '@/components/Model'
+import EditForm from '@/components/Model/EditForm.vue'
 import { useModelStore, useNotificationStore } from '@/stores'
 import ItemId from '@/components/ItemId.vue'
 
@@ -64,6 +68,7 @@ export default defineComponent({
   },
   components: {
     Model,
+    EditForm,
     ItemId
   },
   data: () => ({
diff --git a/tests/unit/stores/model.spec.js b/tests/unit/stores/model.spec.js
index 4a76fc9a79b7710ba6d535dc35ef0c11db6accc6..7d8112b77dfd8698c8760c2330222af7c7077e89 100644
--- a/tests/unit/stores/model.spec.js
+++ b/tests/unit/stores/model.spec.js
@@ -50,6 +50,27 @@ describe('model', () => {
       })
     })
 
+    describe('updateModel', () => {
+      it('updates a model', async () => {
+        store.models = { modelid: { id: 'modelid' } }
+        const attrs = { name: 'test', description: 'A' }
+        mock.onPatch('/model/modelid/').reply(201, { id: 'modelid', ...attrs })
+
+        await store.updateModel('modelid', attrs)
+
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'data'])), [
+          {
+            method: 'patch',
+            url: '/model/modelid/',
+            data: attrs
+          }
+        ])
+        assert.deepStrictEqual(store.models, {
+          modelid: { id: 'modelid', name: 'test', description: 'A' }
+        })
+      })
+    })
+
     describe('listModels', () => {
       it('lists stored models with filters', async () => {
         const reply = {