diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
index bdb0a94d..611f0204 100644
--- a/apps/portal/src/pages/resumes/[resumeId].tsx
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -4,12 +4,14 @@ import Error from 'next/error';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useSession } from 'next-auth/react';
+import { useState } from 'react';
 import {
   AcademicCapIcon,
   BriefcaseIcon,
   CalendarIcon,
   InformationCircleIcon,
   MapPinIcon,
+  PencilSquareIcon,
   StarIcon,
 } from '@heroicons/react/20/solid';
 import { Spinner } from '@tih/ui';
@@ -20,6 +22,8 @@ import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableTe
 
 import { trpc } from '~/utils/trpc';
 
+import SubmitResumeForm from './submit';
+
 export default function ResumeReviewPage() {
   const ErrorPage = (
     <Error statusCode={404} title="Requested resume does not exist" />
@@ -46,6 +50,11 @@ export default function ResumeReviewPage() {
     },
   });
 
+  const userIsOwner =
+    session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
+
+  const [isEditMode, setIsEditMode] = useState(false);
+
   const onStarButtonClick = () => {
     if (session?.user?.id == null) {
       router.push('/api/auth/signin');
@@ -65,6 +74,30 @@ export default function ResumeReviewPage() {
     }
   };
 
+  const onEditButtonClick = () => {
+    setIsEditMode(true);
+  };
+
+  if (isEditMode && detailsQuery.data != null) {
+    return (
+      <SubmitResumeForm
+        initFormDetails={{
+          additionalInfo: detailsQuery.data.additionalInfo ?? '',
+          experience: detailsQuery.data.experience,
+          location: detailsQuery.data.location,
+          resumeId: resumeId as string,
+          role: detailsQuery.data.role,
+          title: detailsQuery.data.title,
+          url: detailsQuery.data.url,
+        }}
+        onClose={() => {
+          utils.invalidateQueries(['resumes.resume.findOne']);
+          setIsEditMode(false);
+        }}
+      />
+    );
+  }
+
   return (
     <>
       {(detailsQuery.isError || detailsQuery.data === null) && ErrorPage}
@@ -80,45 +113,46 @@ export default function ResumeReviewPage() {
             <title>{detailsQuery.data.title}</title>
           </Head>
           <main className="h-[calc(100vh-2rem)] flex-1 overflow-y-auto p-4">
-            <div className="flex flex-row space-x-8">
+            <div className="flex space-x-8">
               <h1 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
                 {detailsQuery.data.title}
               </h1>
-              <button
-                className={clsx(
-                  detailsQuery.data?.stars.length
-                    ? 'z-10 border-indigo-500 outline-none ring-1 ring-indigo-500'
-                    : '',
-                  'isolate inline-flex max-h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50  disabled:hover:bg-white',
+              <div className="flex gap-4">
+                <button
+                  className="isolate inline-flex h-10 items-center space-x-4 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50  disabled:hover:bg-white"
+                  disabled={starMutation.isLoading || unstarMutation.isLoading}
+                  type="button"
+                  onClick={onStarButtonClick}>
+                  <span className="relative inline-flex">
+                    <div className="-ml-1 mr-2 h-5 w-5">
+                      {starMutation.isLoading || unstarMutation.isLoading ? (
+                        <Spinner className="mt-0.5" size="xs" />
+                      ) : (
+                        <StarIcon
+                          aria-hidden="true"
+                          className={clsx(
+                            detailsQuery.data?.stars.length
+                              ? 'text-orange-400'
+                              : 'text-gray-400',
+                          )}
+                        />
+                      )}
+                    </div>
+                    Star
+                  </span>
+                  <span className="relative -ml-px inline-flex">
+                    {detailsQuery.data?._count.stars}
+                  </span>
+                </button>
+                {userIsOwner && (
+                  <button
+                    className="p h-10 rounded-md border border-gray-300 bg-white py-1 px-2 text-center"
+                    type="button"
+                    onClick={onEditButtonClick}>
+                    <PencilSquareIcon className="h-6 w-6 text-indigo-500 hover:text-indigo-300" />
+                  </button>
                 )}
-                disabled={
-                  session?.user === undefined ||
-                  starMutation.isLoading ||
-                  unstarMutation.isLoading
-                }
-                type="button"
-                onClick={onStarButtonClick}>
-                <span className="relative inline-flex">
-                  <div className="-ml-1 mr-2 h-5 w-5">
-                    {starMutation.isLoading || unstarMutation.isLoading ? (
-                      <Spinner className="mt-0.5" size="xs" />
-                    ) : (
-                      <StarIcon
-                        aria-hidden="true"
-                        className={clsx(
-                          detailsQuery.data?.stars.length
-                            ? 'text-orange-400'
-                            : 'text-gray-400',
-                        )}
-                      />
-                    )}
-                  </div>
-                  Star
-                </span>
-                <span className="relative -ml-px inline-flex">
-                  {detailsQuery.data?._count.stars}
-                </span>
-              </button>
+              </div>
             </div>
             <div className="flex flex-col pt-1 lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
               <div className="mt-2 flex items-center text-sm text-gray-500">
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx
index 49763d8d..12568b25 100644
--- a/apps/portal/src/pages/resumes/submit.tsx
+++ b/apps/portal/src/pages/resumes/submit.tsx
@@ -60,8 +60,26 @@ const selectors: Array<SelectorOptions> = [
   { key: 'location', label: 'Location', options: LOCATION },
 ];
 
-export default function SubmitResumeForm() {
-  const [resumeFile, setResumeFile] = useState<File | null>();
+type InitFormDetails = {
+  additionalInfo?: string;
+  experience: string;
+  location: string;
+  resumeId: string;
+  role: string;
+  title: string;
+  url: string;
+};
+
+type Props = Readonly<{
+  initFormDetails?: InitFormDetails | null;
+  onClose: () => void;
+}>;
+
+export default function SubmitResumeForm({
+  initFormDetails,
+  onClose = () => undefined,
+}: Props) {
+  const [resumeFile, setResumeFile] = useState<File | null>(null);
   const [isLoading, setIsLoading] = useState(false);
   const [invalidFileUploadError, setInvalidFileUploadError] = useState<
     string | null
@@ -70,7 +88,8 @@ export default function SubmitResumeForm() {
 
   const { data: session, status } = useSession();
   const router = useRouter();
-  const resumeCreateMutation = trpc.useMutation('resumes.resume.user.create');
+  const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert');
+  const isNewForm = initFormDetails == null;
 
   const {
     register,
@@ -81,6 +100,7 @@ export default function SubmitResumeForm() {
   } = useForm<IFormInput>({
     defaultValues: {
       isChecked: false,
+      ...initFormDetails,
     },
   });
 
@@ -89,7 +109,9 @@ export default function SubmitResumeForm() {
       if (fileRejections.length === 0) {
         setInvalidFileUploadError('');
         setResumeFile(acceptedFiles[0]);
-        setValue('file', acceptedFiles[0]);
+        setValue('file', acceptedFiles[0], {
+          shouldDirty: true,
+        });
       } else {
         setInvalidFileUploadError(FILE_UPLOAD_ERROR);
       }
@@ -106,6 +128,30 @@ export default function SubmitResumeForm() {
     onDrop: onFileDrop,
   });
 
+  const fetchFilePdf = useCallback(async () => {
+    const fileUrl = initFormDetails?.url;
+
+    if (fileUrl == null) {
+      return;
+    }
+
+    const data = await axios
+      .get(fileUrl, {
+        responseType: 'blob',
+      })
+      .then((res) => res.data);
+
+    const keyAndFileName = fileUrl.substring(fileUrl.indexOf('resumes'));
+    const fileName = keyAndFileName.substring(keyAndFileName.indexOf('-') + 1);
+
+    const file = new File([data], fileName);
+    setResumeFile(file);
+    setValue('file', file, {
+      shouldDirty: false,
+    });
+  }, [initFormDetails?.url, setValue]);
+
+  // Route user to sign in if not logged in
   useEffect(() => {
     if (status !== 'loading') {
       if (session?.user?.id == null) {
@@ -114,6 +160,11 @@ export default function SubmitResumeForm() {
     }
   }, [router, session, status]);
 
+  // Fetch initial file PDF for edit form
+  useEffect(() => {
+    fetchFilePdf();
+  }, [fetchFilePdf]);
+
   const onSubmit: SubmitHandler<IFormInput> = async (data) => {
     if (resumeFile == null) {
       return;
@@ -131,10 +182,11 @@ export default function SubmitResumeForm() {
     });
     const { url } = res.data;
 
-    resumeCreateMutation.mutate(
+    resumeUpsertMutation.mutate(
       {
         additionalInfo: data.additionalInfo,
         experience: data.experience,
+        id: initFormDetails?.resumeId,
         location: data.location,
         role: data.role,
         title: data.title,
@@ -148,19 +200,26 @@ export default function SubmitResumeForm() {
           setIsLoading(false);
         },
         onSuccess() {
-          router.push('/resumes/browse');
+          if (isNewForm) {
+            router.push('/resumes/browse');
+          } else {
+            onClose();
+          }
         },
       },
     );
   };
 
   const onClickClear = () => {
-    if (isDirty || resumeFile != null) {
+    if (isDirty) {
       setIsDialogShown(true);
+    } else {
+      onClose();
     }
   };
 
   const onClickResetDialog = () => {
+    onClose();
     setIsDialogShown(false);
     reset();
     setResumeFile(null);
@@ -227,7 +286,11 @@ export default function SubmitResumeForm() {
                 onClick={() => setIsDialogShown(false)}
               />
             }
-            title="Are you sure you want to clear?"
+            title={
+              isNewForm
+                ? 'Are you sure you want to clear?'
+                : 'Are you sure you want to leave?'
+            }
             onClose={() => setIsDialogShown(false)}>
             Note that your current input will not be saved!
           </Dialog>
@@ -346,7 +409,7 @@ export default function SubmitResumeForm() {
                 <Button
                   addonPosition="start"
                   disabled={isLoading}
-                  label="Clear"
+                  label={isNewForm ? 'Clear' : 'Cancel'}
                   variant="tertiary"
                   onClick={onClickClear}
                 />
diff --git a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
index b443c3e2..366385ad 100644
--- a/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
+++ b/apps/portal/src/server/router/resumes/resumes-resume-user-router.ts
@@ -5,11 +5,12 @@ import { createProtectedRouter } from '../context';
 import type { Resume } from '~/types/resume';
 
 export const resumesResumeUserRouter = createProtectedRouter()
-  .mutation('create', {
+  .mutation('upsert', {
     // TODO: Use enums for experience, location, role
     input: z.object({
       additionalInfo: z.string().optional(),
       experience: z.string(),
+      id: z.string().optional(),
       location: z.string(),
       role: z.string(),
       title: z.string(),
@@ -17,11 +18,29 @@ export const resumesResumeUserRouter = createProtectedRouter()
     }),
     async resolve({ ctx, input }) {
       const userId = ctx.session?.user.id;
-      return await ctx.prisma.resumesResume.create({
-        data: {
-          ...input,
+
+      return await ctx.prisma.resumesResume.upsert({
+        create: {
+          additionalInfo: input.additionalInfo,
+          experience: input.experience,
+          location: input.location,
+          role: input.role,
+          title: input.title,
+          url: input.url,
+          userId,
+        },
+        update: {
+          additionalInfo: input.additionalInfo,
+          experience: input.experience,
+          location: input.location,
+          role: input.role,
+          title: input.title,
+          url: input.url,
           userId,
         },
+        where: {
+          id: input.id ?? '',
+        },
       });
     },
   })