diff --git a/i18n/en.json b/i18n/en.json
index 578fe9a115..b9331df5db 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -601,6 +601,7 @@
   "cannot_undo_this_action": "You cannot undo this action!",
   "cannot_update_the_description": "Cannot update the description",
   "change_date": "Change date",
+  "change_description": "Change description",
   "change_display_order": "Change display order",
   "change_expiration_time": "Change expiration time",
   "change_location": "Change location",
@@ -794,6 +795,8 @@
   "edit_avatar": "Edit avatar",
   "edit_date": "Edit date",
   "edit_date_and_time": "Edit date and time",
+  "edit_description": "Edit description",
+  "edit_description_prompt": "Please select a new description:",
   "edit_exclusion_pattern": "Edit exclusion pattern",
   "edit_faces": "Edit faces",
   "edit_import_path": "Edit import path",
@@ -882,6 +885,7 @@
     "unable_to_archive_unarchive": "Unable to {archived, select, true {archive} other {unarchive}}",
     "unable_to_change_album_user_role": "Unable to change the album user's role",
     "unable_to_change_date": "Unable to change date",
+    "unable_to_change_description": "Unable to change description",
     "unable_to_change_favorite": "Unable to change favorite for asset",
     "unable_to_change_location": "Unable to change location",
     "unable_to_change_password": "Unable to change password",
diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
index 39d7cd996f..571badf029 100644
--- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart
+++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
@@ -14,6 +14,7 @@ class AssetBulkUpdateDto {
   /// Returns a new [AssetBulkUpdateDto] instance.
   AssetBulkUpdateDto({
     this.dateTimeOriginal,
+    this.description,
     this.duplicateId,
     this.ids = const [],
     this.isFavorite,
@@ -31,6 +32,14 @@ class AssetBulkUpdateDto {
   ///
   String? dateTimeOriginal;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  String? description;
+
   String? duplicateId;
 
   List<String> ids;
@@ -80,6 +89,7 @@ class AssetBulkUpdateDto {
   @override
   bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
     other.dateTimeOriginal == dateTimeOriginal &&
+    other.description == description &&
     other.duplicateId == duplicateId &&
     _deepEquality.equals(other.ids, ids) &&
     other.isFavorite == isFavorite &&
@@ -92,6 +102,7 @@ class AssetBulkUpdateDto {
   int get hashCode =>
     // ignore: unnecessary_parenthesis
     (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
+    (description == null ? 0 : description!.hashCode) +
     (duplicateId == null ? 0 : duplicateId!.hashCode) +
     (ids.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
@@ -101,7 +112,7 @@ class AssetBulkUpdateDto {
     (visibility == null ? 0 : visibility!.hashCode);
 
   @override
-  String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
+  String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, description=$description, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -110,6 +121,11 @@ class AssetBulkUpdateDto {
     } else {
     //  json[r'dateTimeOriginal'] = null;
     }
+    if (this.description != null) {
+      json[r'description'] = this.description;
+    } else {
+    //  json[r'description'] = null;
+    }
     if (this.duplicateId != null) {
       json[r'duplicateId'] = this.duplicateId;
     } else {
@@ -154,6 +170,7 @@ class AssetBulkUpdateDto {
 
       return AssetBulkUpdateDto(
         dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
+        description: mapValueOfType<String>(json, r'description'),
         duplicateId: mapValueOfType<String>(json, r'duplicateId'),
         ids: json[r'ids'] is Iterable
             ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index e7bf81ce3e..5de3987367 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -8605,6 +8605,9 @@
           "dateTimeOriginal": {
             "type": "string"
           },
+          "description": {
+            "type": "string"
+          },
           "duplicateId": {
             "nullable": true,
             "type": "string"
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 1d3a04da44..c293b2aa6c 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -431,6 +431,7 @@ export type AssetMediaResponseDto = {
 };
 export type AssetBulkUpdateDto = {
     dateTimeOriginal?: string;
+    description?: string;
     duplicateId?: string | null;
     ids: string[];
     isFavorite?: boolean;
diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts
index 0789633878..940cfbf9cc 100644
--- a/server/src/dtos/asset.dto.ts
+++ b/server/src/dtos/asset.dto.ts
@@ -54,6 +54,10 @@ export class UpdateAssetBase {
   @Max(5)
   @Min(-1)
   rating?: number;
+
+  @Optional()
+  @IsString()
+  description?: string;
 }
 
 export class AssetBulkUpdateDto extends UpdateAssetBase {
@@ -65,10 +69,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
 }
 
 export class UpdateAssetDto extends UpdateAssetBase {
-  @Optional()
-  @IsString()
-  description?: string;
-
   @ValidateUUID({ optional: true, nullable: true })
   livePhotoVideoId?: string | null;
 }
diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts
index 556641fdb0..bc73ff6410 100644
--- a/server/src/services/asset.service.ts
+++ b/server/src/services/asset.service.ts
@@ -108,13 +108,21 @@ export class AssetService extends BaseService {
   }
 
   async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
-    const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
+    const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto;
     await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
 
-    if (dateTimeOriginal !== undefined || latitude !== undefined || longitude !== undefined) {
-      await this.assetRepository.updateAllExif(ids, { dateTimeOriginal, latitude, longitude });
+    if (
+      description !== undefined ||
+      dateTimeOriginal !== undefined ||
+      latitude !== undefined ||
+      longitude !== undefined
+    ) {
+      await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude });
       await this.jobRepository.queueAll(
-        ids.map((id) => ({ name: JobName.SIDECAR_WRITE, data: { id, dateTimeOriginal, latitude, longitude } })),
+        ids.map((id) => ({
+          name: JobName.SIDECAR_WRITE,
+          data: { id, description, dateTimeOriginal, latitude, longitude },
+        })),
       );
     }
 
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 09218d3c47..468fbe6d41 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -8,6 +8,7 @@
   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -323,6 +324,7 @@
       <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
         <DownloadAction menuItem />
         <ChangeDate menuItem />
+        <ChangeDescription menuItem />
         <ChangeLocation menuItem />
         <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleDeleteOrArchiveAssets} />
         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
diff --git a/web/src/lib/components/photos-page/actions/change-description-action.svelte b/web/src/lib/components/photos-page/actions/change-description-action.svelte
new file mode 100644
index 0000000000..129d327fb9
--- /dev/null
+++ b/web/src/lib/components/photos-page/actions/change-description-action.svelte
@@ -0,0 +1,37 @@
+<script lang="ts">
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import AssetUpdateDecriptionConfirmModal from '$lib/modals/AssetUpdateDecriptionConfirmModal.svelte';
+  import { user } from '$lib/stores/user.store';
+  import { getSelectedAssets } from '$lib/utils/asset-utils';
+  import { handleError } from '$lib/utils/handle-error';
+  import { updateAssets } from '@immich/sdk';
+  import { mdiText } from '@mdi/js';
+  import { t } from 'svelte-i18n';
+  import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
+  import { getAssetControlContext } from '../asset-select-control-bar.svelte';
+
+  interface Props {
+    menuItem?: boolean;
+  }
+
+  let { menuItem = false }: Props = $props();
+  const { clearSelect, getOwnedAssets } = getAssetControlContext();
+
+  const handleUpdateDescription = async () => {
+    const description = await modalManager.show(AssetUpdateDecriptionConfirmModal, {});
+    if (description) {
+      const ids = getSelectedAssets(getOwnedAssets(), $user);
+
+      try {
+        await updateAssets({ assetBulkUpdateDto: { ids, description } });
+      } catch (error) {
+        handleError(error, $t('errors.unable_to_change_description'));
+      }
+      clearSelect();
+    }
+  };
+</script>
+
+{#if menuItem}
+  <MenuOption text={$t('change_description')} icon={mdiText} onClick={() => handleUpdateDescription()} />
+{/if}
diff --git a/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte
new file mode 100644
index 0000000000..4d5a81f5fa
--- /dev/null
+++ b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte
@@ -0,0 +1,29 @@
+<script lang="ts">
+  import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
+  import { Input } from '@immich/ui';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    onClose: (description?: string) => void;
+  }
+
+  let { onClose }: Props = $props();
+
+  let description = $state('');
+</script>
+
+<ConfirmModal
+  confirmColor="primary"
+  title={$t('edit_description')}
+  prompt={$t('edit_description_prompt')}
+  onClose={(confirmed) => (confirmed ? onClose(description) : onClose())}
+>
+  {#snippet promptSnippet()}
+    <div class="flex flex-col text-start gap-2">
+      <div class="flex flex-col">
+        <label for="description">{$t('description')}</label>
+        <Input class="immich-form-input" id="description" bind:value={description} />
+      </div>
+    </div>
+  {/snippet}
+</ConfirmModal>
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 088d3dae97..e46ad0fc77 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -13,6 +13,7 @@
   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -478,6 +479,7 @@
           <DownloadAction menuItem filename="{album.albumName}.zip" />
           {#if assetInteraction.isAllUserOwned}
             <ChangeDate menuItem />
+            <ChangeDescription menuItem />
             <ChangeLocation menuItem />
             {#if assetInteraction.selectedAssets.length === 1}
               <MenuOption
diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 2ed4b0ada7..598a849a10 100644
--- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -3,6 +3,7 @@
   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -59,6 +60,7 @@
     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
       <DownloadAction menuItem />
       <ChangeDate menuItem />
+      <ChangeDescription menuItem />
       <ChangeLocation menuItem />
       <ArchiveAction
         menuItem
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 2daf63b9e3..df17e1e15c 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -8,6 +8,7 @@
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -115,6 +116,7 @@
       <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
         <DownloadAction menuItem />
         <ChangeDate menuItem />
+        <ChangeDescription menuItem />
         <ChangeLocation menuItem />
         <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 1dc213729d..ea726d783a 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -11,6 +11,7 @@
   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -515,6 +516,7 @@
           onClick={handleReassignAssets}
         />
         <ChangeDate menuItem />
+        <ChangeDescription menuItem />
         <ChangeLocation menuItem />
         <ArchiveAction
           menuItem
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index 20f4ca0abc..162beaf8f5 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -5,6 +5,7 @@
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -142,6 +143,7 @@
         />
       {/if}
       <ChangeDate menuItem />
+      <ChangeDescription menuItem />
       <ChangeLocation menuItem />
       <ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
       {#if $preferences.tags.enabled}
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 5f995b9a7a..813683244e 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -9,6 +9,7 @@
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
+  import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -358,6 +359,7 @@
           <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
             <DownloadAction menuItem />
             <ChangeDate menuItem />
+            <ChangeDescription menuItem />
             <ChangeLocation menuItem />
             <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
             {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}